﻿using System;
using System.IO;
using System.Security.Cryptography;
using Flowertwig.Utils.Compression.General;

namespace Flowertwig.Utils.Compression.ZLibExtensions
{
    /// <summary>
    /// An input buffer customised for use by <see cref="InflaterInputStream"/>
    /// </summary>
    /// <remarks>
    /// The buffer supports decryption of incoming data.
    /// </remarks>
    public class InflaterInputBuffer
    {
        /// <summary>
        /// Initialise a new instance of <see cref="InflaterInputBuffer"/>
        /// </summary>
        /// <param name="stream">The stream to buffer.</param>
        public InflaterInputBuffer(Stream stream)
        {
            inputStream = stream;
            rawData = new byte[4096];
            clearText = rawData;
        }
		
        /// <summary>
        /// Get the length of bytes bytes in the <see cref="RawData"/>
        /// </summary>
        public int RawLength
        {
            get { 
                return rawLength; 
            }
        }
		
        /// <summary>
        /// Get the contents of the raw data buffer.
        /// </summary>
        /// <remarks>This may contain encrypted data.</remarks>
        public byte[] RawData
        {
            get {
                return rawData;
            }
        }
		
        /// <summary>
        /// Get the number of useable bytes in <see cref="ClearText"/>
        /// </summary>
        public int ClearTextLength
        {
            get {
                return clearTextLength;
            }
        }
		
        /// <summary>
        /// Get the contents of the clear text buffer.
        /// </summary>
        public byte[] ClearText
        {
            get {
                return clearText;
            }
        }
		
        /// <summary>
        /// Get/set the number of bytes available
        /// </summary>
        public int Available
        {
            get { return available; }
            set { available = value; }
        }

        /// <summary>
        /// Call <see cref="Inflater.SetInput"/> passing the current clear text buffer contents.
        /// </summary>
        /// <param name="inflater">The inflater to set input for.</param>
        public void SetInflaterInput(Inflater inflater)
        {
            if ( available > 0 ) {
                inflater.SetInput(clearText, clearTextLength - available, available);
                available = 0;
            }
        }

        /// <summary>
        /// Fill the buffer from the underlying input stream.
        /// </summary>
        public void Fill()
        {
            rawLength = 0;
            int toRead = rawData.Length;
			
            while (toRead > 0) {
                int count = inputStream.Read(rawData, rawLength, toRead);
                if ( count <= 0 ) {
                    if (rawLength == 0) {
                        throw new SharpZipBaseException("Unexpected EOF"); 
                    }
                    break;
                }
                rawLength += count;
                toRead -= count;
            }

            if ( cryptoTransform != null ) {
                clearTextLength = cryptoTransform.TransformBlock(rawData, 0, rawLength, clearText, 0);
            }
            else {
                clearTextLength = rawLength;
            }

            available = clearTextLength;
        }
		
        /// <summary>
        /// Read a buffer directly from the input stream
        /// </summary>
        /// <param name="buffer">The buffer to fill</param>
        /// <returns>Returns the number of bytes read.</returns>
        public int ReadRawBuffer(byte[] buffer)
        {
            return ReadRawBuffer(buffer, 0, buffer.Length);
        }

        /// <summary>
        /// Read a buffer directly from the input stream
        /// </summary>
        /// <param name="outBuffer">The buffer to read into</param>
        /// <param name="offset">The offset to start reading data into.</param>
        /// <param name="length">The number of bytes to read.</param>
        /// <returns>Returns the number of bytes read.</returns>
        public int ReadRawBuffer(byte[] outBuffer, int offset, int length)
        {
            if ( length <= 0 ) {
                throw new ArgumentOutOfRangeException("length");
            }
			
            int currentOffset = offset;
            int currentLength = length;
			
            while ( currentLength > 0 ) {
                if ( available <= 0 ) {
                    Fill();
                    if (available <= 0) {
                        return 0;
                    }
                }
                int toCopy = Math.Min(currentLength, available);
                System.Array.Copy(rawData, rawLength - (int)available, outBuffer, currentOffset, toCopy);
                currentOffset += toCopy;
                currentLength -= toCopy;
                available -= toCopy;
            }
            return length;
        }
		
        /// <summary>
        /// Read clear text data from the input stream.
        /// </summary>
        /// <param name="outBuffer">The buffer to add data to.</param>
        /// <param name="offset">The offset to start adding data at.</param>
        /// <param name="length">The number of bytes to read.</param>
        /// <returns>Returns the number of bytes actually read.</returns>
        public int ReadClearTextBuffer(byte[] outBuffer, int offset, int length)
        {
            if ( length <= 0 ) {
                throw new ArgumentOutOfRangeException("length");
            }
			
            int currentOffset = offset;
            int currentLength = length;
			
            while ( currentLength > 0 ) {
                if ( available <= 0 ) {
                    Fill();
                    if (available <= 0) {
                        return 0;
                    }
                }
				
                int toCopy = Math.Min(currentLength, available);
                System.Array.Copy(clearText, clearTextLength - (int)available, outBuffer, currentOffset, toCopy);
                currentOffset += toCopy;
                currentLength -= toCopy;
                available -= toCopy;
            }
            return length;
        }
		
        /// <summary>
        /// Read a byte from the input stream.
        /// </summary>
        /// <returns>Returns the byte read.</returns>
        public int ReadLeByte()
        {
            if (available <= 0) {
                Fill();
                if (available <= 0) {
                    throw new ZipException("EOF in header");
                }
            }
            byte result = (byte)(rawData[rawLength - available] & 0xff);
            available -= 1;
            return result;
        }
		
        /// <summary>
        /// Read an unsigned short in little endian byte order.
        /// </summary>
        public int ReadLeShort()
        {
            return ReadLeByte() | (ReadLeByte() << 8);
        }
		
        /// <summary>
        /// Read an int in little endian byte order.
        /// </summary>
        public int ReadLeInt()
        {
            return ReadLeShort() | (ReadLeShort() << 16);
        }
		
        /// <summary>
        /// Read an int baseInputStream little endian byte order.
        /// </summary>
        public long ReadLeLong()
        {
            return ReadLeInt() | (ReadLeInt() << 32);
        }

        /// <summary>
        /// Get/set the <see cref="ICryptoTransform"/> to apply to any data.
        /// </summary>
        /// <remarks>Set this value to null to have no transform applied.</remarks>
        public ICryptoTransform CryptoTransform
        {
            set { 
                cryptoTransform = value;
                if ( cryptoTransform != null ) {
                    if ( rawData == clearText ) {
                        if ( internalClearText == null ) {
                            internalClearText = new byte[4096];
                        }
                        clearText = internalClearText;
                    }
                    clearTextLength = rawLength;
                    if ( available > 0 ) {
                        cryptoTransform.TransformBlock(rawData, rawLength - available, available, clearText, rawLength - available);
                    }
                } else {
                    clearText = rawData;
                    clearTextLength = rawLength;
                }
            }
        }

        #region Instance Fields
        int rawLength;
        byte[] rawData;
		
        int clearTextLength;
        byte[] clearText;
		
        byte[] internalClearText;
		
        int available;
		
        ICryptoTransform cryptoTransform;
        Stream inputStream;
        #endregion
    }
}